<?php if ( ! defined('BASEPATH')) exit('This file does not run directly. Sorry.'); ?>
<?php
/**
 * Kalaisoo.
 *
 * A lightweight no-framework framework written in PHP.
 *
 * @package Kalaisoo
 * @author $Author: info@sah-company.com $
 * @version $Id: user.php 151 2012-02-01 01:08:39Z info@sah-company.com $
 */

/**
 * Load dependencies.
 */
Registry::instance()->loader->model(array(
	'rbac_role'
	)
);

/**
 * Kalaisoo User Model Class.
 *
 * @package Kalaisoo
 * @subpackage Model
 * @author $Author: info@sah-company.com $
 */
class Model_User extends Model
{
	/**
	 * name of the user name field.
	 *
	 * @const string
	 */
	const FN_NAME = 'name';

	/**
	 * name of the password field.
	 *
	 * @const string
	 */
	const FN_PW = 'pw';
	
	/**
	 * time frame to prevent repeated action on a bean being autoBlog(ed) about
	 *
	 * @const int
	 */
	const TIMEFRAME_REPEATED_ACTION = 1800; // defaults to 30 minutes

	/**
	 * constructs a new Model_User.
	 */
	public function construct()
	{
		parent::__construct();
	}
	
	/**
	 * returns the tags of a bean.
	 *
	 * @return array
	 */
	public function getTags() {
		return array(
			$this->bean->email,
			$this->bean->name,
			$this->bean->location
		);
	}
	
	/**
	 * returns an array with special actions the model can take.
	 *
	 * @return array
	 */
	public function actions()
	{
		if ( ! $this->bean->getId()) return array();
		return array(
			//'avatar' => true,
			'friendme' => true,
			'profile' => true
		);
	}
	
	/**
	 * redirects to this users profile.
	 */
	public function profile()
	{
		$this->controller()->redirect(ahref(sprintf('%s/user/profile/%s', Registry::instance()->language, $this->bean->getId())));
	}
	
	/**
	 * relates this user to another as a "friend".
	 *
	 */
	public function friendme()
	{
		try {
			R::associate($this->controller()->user(), $this->bean);
			$this->controller->data['messages'][] = __('user_friendme_success');
			$this->controller()->user()->autoBlog($this->bean, 'friendme');
		} catch (Exception $e) {
			$this->controller->data['errors'][] = __('user_friendme_failed');
		}
		return $this->controller()->view($this->bean->getMeta('type'), $this->bean->getId());
	}
	
	/**
	 * returns the users name.
	 *
	 * @return string
	 */
	public function name()
	{
		if ($this->bean->name) return $this->bean->name;
		return $this->bean->email;
	}
	
	/**
	 * returns the number of unseen blog entries or space.
	 *
	 * @return string
	 */
	public function notification()
	{
		if (isset($_SESSION['blog_counter'])) return $_SESSION['blog_counter'];
		return '';
	}
	
	/**
	 * returns the location.
	 *
	 * @return string
	 */
	public function location($refresh = false)
	{
		if ($this->bean->location) return $this->bean->location;
		return __('unknown_location');
	}
	
	/**
	 * returns the url or src to the addressbook avatar image.
	 */
	public function gravatar()
	{
		return $this->gravatarWorkhorse($this->bean->avatar);
	}
	
	/**
	 * returns the path to a avatar image
	 *
	 * @param string $image
	 */
	public static function gravatarWorkhorse($image = null)
	{
		$pathToImage = ($image === null) ? 'user.png' : $image;
		return ahref(Registry::instance()->config['uploads']['path'].'/'.$pathToImage);
	}
	
	/**
	 * displays a form to upload a image and crop it.
	 *
	 * This uses jCrop to allow cropping the image.
	 */
	public function avatar()
	{
		if ($this->controller()->input->submit) {
			if ($avatar = Filesystem::moveUploadedFile('uploaded_file', 'avatar_'.$this->bean->getId())) {
				$this->bean->avatar = $avatar;
				if ( ! $this->cropAvatar()) {
					$this->controller()->data['errors'][] = __('user_failed_avatar_crop');
				}
			} else {
				if ( ! $this->cropAvatar($this->controller()->input->x, $this->controller()->input->y, $this->controller()->input->w, $this->controller()->input->h)) {
					$this->controller()->data['errors'][] = __('user_failed_avatar_crop');
				}
			}
			try {
				R::store($this->bean);
				$this->controller()->redirect(ahref(sprintf('%s/editor/avatar/%s/%s/', Registry::instance()->language, 'user', $this->bean->getId())));
			} catch (Exception $e) {
				$this->controller()->data['errors'][] = __('user_failed_avatar_upload');
			}
		}
		$this->controller()->data['scripts'][] = 'jquery/jcrop/jquery.Jcrop.min';
		$this->controller()->data['scripts'][] = 'avatar';
		$this->controller()->data['styles'][] = 'jquery.Jcrop';
		$this->controller()->data['url'] = ahref(sprintf('{language}/editor/avatar/%s/%s', 'user', $this->bean->getId()));
		$this->controller()->data['submit'] = __('user_submit_avatar');
		$this->controller()->addBreadcrumbs('avatar');
		$this->controller()->toolbarActions('avatar', array('index')); // tools, but not index
		$this->controller()->data['errors'] = array_merge($this->controller()->data['errors'], $this->error());
		$this->controller()->template->view('user/avatar', $this->controller()->data);
	}
	
	/**
	 * crops the image and saves it.
	 *
	 * @param int $x
	 * @param int $y
	 * @param int $w
	 * @param int $h
	 */
	protected function cropAvatar($x = 0, $y = 0, $w = null, $h = null, $compression = 100)
	{
		require BASEPATH.'/vendor/simpleimage/simpleimage'.EXT;
		$width = $height = AVATAR_SIZE * 10;
		$src = BASEPATH.'/'.Registry::instance()->config['uploads']['path'].'/'.$this->bean->avatar;
		$image = new SimpleImage();
		$image->setCompression($compression);
		$image->load($src);
		if ($w == null && $h == null) {
			$image->resizeToWidth($width);
			return $image->save($src);
		}
		if ( ! $image = imagecreatefromjpeg($src)) return false;
        if ( ! $destination = imagecreatetruecolor($width, $height)) return false;
        imagecopyresampled($destination, $image, 0, 0, $x, $y, $width, $height, $w, $h);
        return imagejpeg($destination, $src, $compression);
	}
	
	/**
	 * loads the array $this->gestalt of this bean for use with Model::gestalt().
	 */
	public function loadGestalten()
	{
		$this->gestalt['default'] = array(
			array(
				'type' => 'hidden',
				'name' => 'avatar',
				'parameters' => array(
				)
			),
			array(
				'type' => 'email',
				'name' => 'email',
				'parameters' => array(
					'placeholder' => __('user_email_placeholder'),
					'required' => 'required'
				)
			),
			array(
				'type' => 'password',
				'name' => 'pw',
				'help' => __('user_pw_help'),
				'parameters' => array(
					'required' => 'required'
				)
			),
			array(
				'type' => 'textarea',
				'name' => 'aboutme',
				'help' => __('user_aboutme_help'),
				'parameters' => array(
					'placeholder' => __('user_aboutme_placeholder'),
					'cols' => 60,
					'rows' => 5
				)				
			),
			array(
				'type' => 'checkbox',
				'name' => 'admin',
				'help' => __('user_admin_help'),
				'parameters' => array(
					'value' => 1
				)
			),
			array(
				'type' => 'text',
				'name' => 'name',
				'parameters' => array(
					'placeholder' => __('user_name_placeholder'),
				)
			),
			array(
				'type' => 'text',
				'name' => 'location',
				'help' => __('user_location_help'),
				'parameters' => array(
					'placeholder' => __('user_location_placeholder'),
				)
			),
			array(
				'type' => 'related',
				'name' => 'rbac_role',
				'help' => __('user_rbac_role_help')
			)
		);
	}
	
	/**
	 * validates the data and returns either true or false.
	 *
	 * @return bool
	 */
	public function validate()
	{
		$valid = true;
		if ( ! filter_var($this->bean->email, FILTER_VALIDATE_EMAIL))
		{
			// email does not validate
			$this->addError('user_email_invalid', 'email');
			$valid = false;
		}
		return $valid;
	}

	/**
	 * open.
	 */
	public function open()
	{
	}

	/**
	 * dispense.
	 */
	public function dispense()
	{
		$this->bean->setMeta('buildcommand.unique', array(array('email')));
	}

	/**
	 * update.
	 */
	public function update()
	{
		if ( ! $this->validate()) {
			throw new Exception('User did not validate');
		}
		// pw hashing only on new user record
		if ( ! $this->bean->getId()) {
			$this->bean->pw = $this->hash($this->bean->pw);
		}
	}
	
	/**
	 * after_update.
	 */
	public function after_update()
	{
	}
	
	/**
	 * delete.
	 */
	public function delete()
	{
	}
	
	/**
	 * after_delete.
	 */
	public function after_delete()
	{
	}
	
	/**
	 * This will return this users startpage which is simply an url.
	 *
	 * @return string
	 */
	public function startpage()
	{
		return Registry::instance()->language.'/user';
	}
	
	/**
	 * revokes the session authorization and regenerates the session id.
	 */
	public function logout()
	{
		$_SESSION['auth'] = null;
		session_regenerate_id(true);
	}
	
	/**
	 * returns wether the user has a valid session or not.
	 *
	 * @return bool
	 */
	public function isAuth()
	{
		if (isset($_SESSION['auth']) && $_SESSION['auth']) {
			return true;
		}
		return false;
	}
	
	/**
	 * returns wether true if the credentials where authenticated against the db or not.
	 *
	 * @param array $credentials
	 * @return bool
	 */
	public function auth(array $credentials)
	{
		if ( ! isset($credentials[self::FN_NAME]) || ! isset($credentials[self::FN_PW])) {
			$this->addError('user_no_credentials');
			return false;
		}
		$found = R::findOne('user', ' email=:email LIMIT 1', array('email' => $credentials[self::FN_NAME]));
		if ( ! $found) {
			$this->addError('user_not_found', 'fn_name');
			return false;
		}
		$this->bean->import($found);
		if ( $this->bean->pw !== $this->hash($credentials[self::FN_PW])) {
			$this->addError('user_pw_wrong', 'fn_pw');
			return false;
		}
		session_regenerate_id(true);
		$_SESSION['auth'] = $this->bean->getId();
		try {
			Registry::instance()->loader->lib('geoip');
			$location = GeoIPFacade::locate($_SERVER['REMOTE_ADDR']);
			if ($location) $found->location = $location->city;
			$found->last_login = time();
			R::store($found);
		} catch (Exception $e) {
			// grr... user could not be stored
			error_log('failed_to_store_user_that_just_logged_in');
		}
		return true;
	}
	
	/**
	 * creates a blog entry base on a action taken on a domains bean and returns true if okay.
	 *
	 * @param mixed $bean
	 * @param string $verb
	 * @return bool
	 */
	public function autoBlog($bean, $action)
	{
		$now = time();
		$autoCode = md5($this->bean->getId().$bean->getMeta('type').$bean->getId().$action);
		
		$until = $now - self::TIMEFRAME_REPEATED_ACTION; // within a time frame of 5 minutes
		$hasBeenAutoBloggedShortlyBefore = R::getCell('SELECT count(id) AS total FROM blog WHERE auto_code = ? AND stamp >= ?', array($autoCode, (int)$until));
		if ($hasBeenAutoBloggedShortlyBefore) return false; // dont: repeated action
		
		$verb = __(strtolower($action).'_verb');
		$user_link = sprintf('<a href="{language}/user/profile/%2$s" class="user">%1$s</a>', $this->name(), $this->bean->getId());
		$domain_link = sprintf('<a href="{language}/editor/index/%2$s" class="label %2$s">%1$s</a>', __($bean->getMeta('type')), $bean->getMeta('type'));
		if ($bean->getId()) {
			$bean_link = sprintf('<a href="{language}/editor/view/%3$s/%2$s/" class="view %3$s">%1$s</a>', $bean->view(), $bean->getId(), $bean->getMeta('type'));
		} else {
			$bean_link = sprintf('<span class="deleted">%1$s</span>', $bean->view());
		}
		$content = sprintf(__('auto_blog_template'), $user_link, $verb, $domain_link, $bean_link);
		try {
			$blog = R::dispense('blog');
			$blog->content = $content;
			$blog->stamp = $now;
			$blog->auto = true;
			$blog->auto_code = $autoCode;
			$blog->location = $this->bean->location;
			R::store($blog);
			if ($this->bean->getId()) R::associate($blog, $this->bean);
			return true;
		} catch (Exception $e) {
			return false;
		}
	}
	
	/**
	 * sends a email to the users email address with a authorization link to change the password.
	 *
	 * @uses Mail
	 *
	 * @param array $credentials
	 * @return bool
	 */
	public function lostpassword(array $credentials)
	{
		if ( ! isset($credentials[self::FN_NAME])) {
			$this->addError('user_no_credentials');
			return false;
		}
		$found = R::findOne('user', ' email=:email LIMIT 1', array('email' => $credentials[self::FN_NAME]));
		if ( ! $found) {
			$this->addError('user_not_found', 'fn_name');
			return false;
		}
		
		// load user and gen new session
		$this->bean->import($found);
		session_regenerate_id(true);
		$_SESSION['auth'] = false;
		
		// generate a auth code for changing password and store it
		$auth = R::dispense('auth');
		$this->bean->salt = null;
		$this->bean->iteration = null;
		$auth->code = $this->hash(md5(uniqid(rand(), true)));
		$auth->user = $this->bean;
		
		if ( ! R::store($auth)) {
			$this->addError('user_auth_not_saved');
			return false;
		}
		
		// load some libs, helpers and language stuff
		Registry::instance()->loader->lib('mail');
		Registry::instance()->loader->helper(array('textile', 'url'));
		Registry::instance()->loader->language('user');
		
		// setup mail
		$mail = new Mail();
		$reg = Registry::instance();
		
		// make the changepassword link
		$link = ahref(sprintf('%s/user/changepassword/%s', $reg->language, $auth->code), true);
		
		// assemble the mail
		$mail->CharSet = $reg->config['mail']['charset'];
		$mail->SetFrom($reg->config['mail']['from']['email'], $reg->config['mail']['from']['name']);
		$mail->AddReplyTo($reg->config['mail']['from']['email'], $reg->config['mail']['from']['name']);
		$mail->AddAddress($this->bean->email, $this->bean->name);
		$mail->Subject = __('user_changepassword_mail_subject');
		$mail->Body = sprintf(__('user_changepassword_mail_template'), $this->name(), $link);

		if ( ! $mail->Send()) {
			$this->addError('user_auth_not_sent');
			return false;
		}
		return true;
	}
	
	/**
	 * changes the password and returns the result of the operation.
	 *
	 * @param array $credentials
	 * @param string $authCode
	 * @return bool
	 */
	public function changepassword(array $credentials, $authCode)
	{
		$found = R::findOne('auth', ' code=:code LIMIT 1', array('code' => $authCode));
		if ( ! $found) {
			$this->addError('user_authcode_not_found');
			return false;
		}
		if ( ! isset($credentials['pw_new']) || ! isset($credentials['pw_repeated'])) {
			$this->addError('user_no_credentials');
			return false;
		}
		if (empty($credentials['pw_new'])) {
			$this->addError('user_pw_empty', 'fn_pw_new');
			return false;
		}
		if ($credentials['pw_new'] !== $credentials['pw_repeated']) {
			$this->addError('user_pw_mismatch', 'fn_pw_repeated');
			return false;
		}
		
		session_regenerate_id(true);
		$_SESSION['auth'] = false;
		
		$this->bean = R::load('user', $found->user->id);
		$this->bean->salt = null;
		$this->bean->iteration = null;
		$this->bean->pw = $this->hash($credentials['pw_new']);
		
		R::store($this->bean);
		
		R::trash($found); // Get rid of the auth code
		
		return true;
	}
	
	/**
	 * returns a hash of the password and sets salt and iteration.
	 *
	 * @param string $hase
	 * @param string $algo_func
	 * @return string
	 */
	public function hash($hash, $algo_func = 'sha1')
	{
		if ($this->bean->salt === null || empty($this->bean->salt)) $this->bean->salt = md5(uniqid(rand(), true));
		if ($this->bean->iteration === null || ! $this->bean->iteration) $this->bean->iteration = rand(1, 1000);
		for ($i = 0; $i <= $this->bean->iteration; $i++) {
			$hash = $algo_func($hash.$this->bean->salt);
		}
		return $hash;
	}
}
?>